<?php
/**
 * Methods and structure to facilitate sync
 */

class SyncProfile {
	private $uuid;
	private $name;
	private $host;
	private $system;
	private $bucket;
	private $local;
	private $remote;
	private $master;
	private $oneshot;
	private $clobber;
	private $progress;
	private $strict;
	private $verbose;
	
	
	private $localtree;
	private $remotetree;
	
	
	const MASTER_LOCAL  = 1;
	const MASTER_REMOTE = 2;
	const MASTER_SYNC = 4;
	
	/**
   * Sets and returns specified variable 
   *
   * @param string $var The variable to set/get.
   * @param string $value The value in which to set the var.
   * @return mixed: value of $this->var.
   */
	private function getSet($var, $value) {
		if (!is_null($value)) {
			$this->$var = $value;
		}
		return $this->$var;
	}
	
	/**
   * Sets and returns uuid 
   *
   * @param string $value value to set
   * @return string: Current value of var
   */
	public function uuid($value = null) {
		return $this->getSet("uuid", $value);
	}
	
	/**
   * Sets and returns name 
   *
   * @param string $value value to set
   * @return string: Current value of var
   */
	public function name($value = null) {
		return $this->getSet("name", $value);
	}
	
	/**
   * Sets and returns host 
   *
   * @param string $value value to set
   * @return string: Current value of var
   */
	public function host($value = null) {
		return $this->getSet("host", $value);
	}
	
	/**
   * Sets and returns system 
   *
   * @param string $value value to set
   * @return string: Current value of var
   */
	public function system($value = null) {
		return $this->getSet("system", $value);
	}
	
	/**
   * Sets and returns bucket 
   *
   * @param string $value value to set
   * @return string: Current value of var
   */
	public function bucket($value = null) {
		return $this->getSet("bucket", $value);
	}
	
	/**
   * Sets and returns local 
   *
   * @param string $value value to set
   * @return string: Current value of var
   */
	public function local($value = null) {
		return $this->getSet("local", $value);
	}
	
	/**
   * Sets and returns remote 
   *
   * @param string $value value to set
   * @return string: Current value of var
   */
	public function remote($value = null) {
		return $this->getSet("remote", $value);
	}
	
	/**
   * Sets and returns master 
   *
   * @param string $value value to set
   * @return string: Current value of var
   */
	public function master($value = null) {
		return $this->getSet("master", $value);
	}
	
	/**
   * Sets and returns domain 
   *
   * @param string $value value to set
   * @return string: Current value of var
   */
	public function domain($value = null) {
		return $this->getSet("domain", $value);
	}
	
	/**
   * Sets and returns one shot state 
   *
   * @param string $value value to set
   * @return string: Current value of var
   */
	public function oneshot($value = null) {
		return $this->getSet("oneshot", $value);
	}
	
	/**
   * Sets and returns clobber state 
   *
   * @param string $value value to set
   * @return string: Current value of var
   */
	public function clobber($value = null) {
		return $this->getSet("clobber", $value);
	}
	
	/**
   * Sets and returns strict
   *
   * @param string $value new strict
   * @return string: strict
   */
	public function strict($value = null) {
		return $this->getSet("strict", $value);
	}
	
	/**
   * Sets and returns progress
   *
   * @param string $value new progress
   * @return string: progress
   */
	public function progress($value = null) {
		return $this->getSet("progress", $value);
	}
	
	
	/**
   * Sets and returns progress
   *
   * @param string $value new progress
   * @return string: progress
   */
	public function verbose($value = null) {
		return $this->getSet("verbose", $value);
	}
	
	/**
   * Validates a list of object properties have been set 
   *
   * @param array $listOfParams Array of properties to validate
   * @return bool: true, success. false, failure
   */
	private function requireProperties($listOfParams) {
		foreach ($listOfParams as $param) {
			if (!isset($this->$param)) {
				throw new Exception (Errors::SyncProfile_MESG_PROPERTY_NOT_SET . " ($param)", Errors::SyncProfile_CODE_PROPERTY_NOT_SET);
			}
		}
		return true;
	}
	
	public function notify($str) {
		if ($this->verbose === true) {
			echo "[VERBOSE] ";
			$trace = debug_backtrace();
			$caller = $trace[1];
			if (isset($caller["class"])) {
				echo $caller["class"] . "->";
			}
			if (isset($caller["function"])) {
				echo $caller["function"] . "()";
			}	
			echo " ($str)\n";
		}
	}
	
	/**
   * Sets up a new profile in the SDB dbase 
   *
   * @return bool: true, success. false, failure
   */
	public function defineNewProfile() {
		$this->requireProperties(array("name", "host", "local", "remote", "bucket", "master", "domain", "clobber"));
		
		$sdb = new SDBUtils();
		
		$hashString = Utils::sha256($this->host() . $this->local() . $this->remote() . $this->master());
		$itemCheck = $sdb->itemGet($this->domain(), $hashString);
		
		// Check to see if the entry already exits
		if (!$itemCheck && isset($itemCheck["profileName"])) {
			throw new Exception (Errors::SyncProfile_MESG_PROFILE_ALREADY_DEF . " as profile name ({$itemCheck["profileName"]})", Errors::SyncProfile_CODE_PROFILE_ALREADY_DEF);
		} elseif ($itemCheck) {
			throw new Exception (Errors::SyncProfile_MESG_PROFILE_ALREADY_DEF . " but cannot reference", Errors::SyncProfile_CODE_PROFILE_ALREADY_DEF);
		}	
		
		// Ensure the name is not already in use
		$countItems = $sdb->domainSelect("SELECT count(*) FROM `$domain` WHERE profileName = '$profileName'");
		if (!is_null($countItems) && $countItems["Domain"]["Count"] > 0) {
			throw new Exception (Errors::SyncProfile_MESG_PROFILE_ALREADY_DEF, Errors::SyncProfile_CODE_PROFILE_ALREADY_DEF);
		}
		
		// Insert the new profile
		$insert = $sdb->itemPut(
			$domain, 
			$hashString, 
			array(
				"hostName" => $hostName,
				"profileName" => $profileName,
				"remoteObject" => $remoteObject,
				"localObject" => $localObject,
				"bucket" => $bucket,
				"master" => $master
			));
		return true;
	}
	
	public function getSyncList() {
		$allSyncProfiles = false;
		if (!is_null($this->host())) {
			$allSyncProfiles = $sdb->domainSelect();//("SELECT * FROM `$domain` WHERE hostName = '" . this->host() . "'");
		} else {
			$allSyncProfiles = $sdb->domainGetAll($domainsdb);
		}
		return $allSyncProfiles;
	}
	
	public function runSync() {
		$this->requireProperties(array("name", "host", "local", "bucket", "master", "domain", "clobber"));
		
		if ($this->master == self::MASTER_LOCAL) {
			echo "Local is master... push to remote\n";
		}
		if ($this->master == self::MASTER_REMOTE) {
			echo "Remote is master... pull from remote\n";
		}
		if ($this->master == self::MASTER_SYNC) {
			echo "Intelligent sync will be attempted... mtimes and logs will be respected\n";
		}
		
		if ($this->clobber) {
			echo "Clobber is set!\n";
		}
		
		// Remove trailling "/"
		if (Utils::charLast($this->local) == "/") {
			$this->local = Utils::charStripLast($this->local);
		}
		
		$s3sdb = new S3SDB($this->domain);
		$s3sdb->verbose = $this->verbose;
		$this->localtree = null;
		echo "Traversing local tree...   ";
		$this->buildLocalTree($this->local);
		
		foreach ($this->localtree as $file => $data) {
			$normalizedName = str_replace($this->local, "", $file);
			if ($normalizedName[0] == "/") {
				$normalizedName = Utils::charStripFirst($normalizedName);
			}
			$localNormal[$normalizedName]["md5"] = $data["md5"];
			$localNormal[$normalizedName]["mtime"] = $data["mtime"];	
		}
		
		echo "Done\n";
		
		echo "Traversing remote tree... ";
		if (!is_null($this->remote)) {
			$this->remotetree = $s3sdb->getBucketObjects($this->remote);
		} else {
			$this->remotetree = $s3sdb->getBucketObjects();
		}
		echo "Done\n";
		if (is_null($this->remotetree)) {
			$this->remotetree = array();
		}
		
		if (!is_null($this->remote)) {
			foreach ($this->remotetree as $file => $data) {
				$normalizedName = str_replace($this->remote, "", $file);
				if ($normalizedName[0] == "/") {
					$normalizedName = Utils::charStripFirst($normalizedName);
				}
				$remoteNormal[$normalizedName]["md5"] = $data["md5"];
				$remoteNormal[$normalizedName]["mtime"] = $data["mtime"];
			}
		} else {
			$remoteNormal = $this->remotetree;
		}
		
		if (!is_array($remoteNormal)) {
			$remoteNormal = array();
		}
		
		if (!is_array($localNormal)) {
			$localNormal = array();
		}
				
		
		
		
		echo "Diffing... ";
		$xor = Utils::array_xor(array_keys($remoteNormal), array_keys($localNormal));
		echo "Done\n";
		
		/*
		echo "localNormal\n";
		print_r($localNormal);
		echo "remoteNormal\n";
		print_r($remoteNormal);
		echo "\nxor\n";
		print_r($xor);
		echo "array_intersect(xor, array_keys(remoteNormal))\n";
		print_r(array_intersect($xor, array_keys($remoteNormal)));
		echo "array_intersect(xor, array_keys(localNormal))\n";
		print_r(array_intersect($xor, array_keys($localNormal)));
		//echo "Xor count: " . count($xor) . "\n";
		return;
		*/
		
		if (count($xor) == 0) {
			echo "No diffs found.\n";
			return;
		}
		
		echo "Generating sync lists...\n";
		$syncList = null;		
		foreach (array_intersect($xor, array_keys($remoteNormal)) as $remote) {
			echo "Testing remote : $remote... ";
			if ($this->master == self::MASTER_LOCAL) {
				echo "master is local... ";
				if ((!isset($localNormal[$remote]["md5"]) && $this->clobber)) {
					echo "local file does not exist (is clobbering the remote): delete";
					$syncList["remoteDelete"][] = $remote;
				} elseif (!isset($localNormal[$remote]["md5"])) {
					echo "local file does not exist (but is not clobbering the remote): do nothing";
				} else {
					echo "Nothing to do.";
				}
			}
			
			if ($this->master == self::MASTER_REMOTE) {
				echo "master is remote... ";
				if (!isset($localNormal[$remote]["md5"])) {
					echo "file does not exist on local: download";
					$syncList["download"][] = $remote;
				} elseif (($localNormal[$remote]["md5"] != $remoteNormal[$remote]["md5"])) {
					echo "MD5s do not match: download";
					$syncList["download"][] = $remote;
				} else {
					echo "Nothing to do.";
				}
			}
			echo "\n";
		}
		
		foreach (array_intersect($xor, array_keys($localNormal)) as $local) {
			echo "Testing local : $local... ";
			if ($this->master == self::MASTER_REMOTE) {
				echo "master is remote... ";
				if (!isset($remoteNormal[$local]["md5"]) && $this->clobber) {
					echo "remote file does not exist (is clobbering the local) : delete";
					$syncList["localDelete"][] = $local;
				} elseif (!isset($remoteNormal[$local]["md5"])) {
					echo "remote file does not exist (but is not clobbering the local): do nothing";
				} else {
					echo "Nothing to do.";
				}
			}
			
			if ($this->master == self::MASTER_LOCAL) {
				echo "master is local... ";
				if (!isset($remoteNormal[$local]["md5"])) {
					echo "file does not exist on remote: upload";
					$syncList["upload"][] = $local;
				} elseif (($remoteNormal[$local]["md5"] != $localNormal[$local]["md5"])) {
					echo "MD5s do not match and local is master : upload";
					$syncList["upload"][] = $local;
				} else {
					echo "Nothing to do...";
				}
			}
			echo "\n";
		}
		echo "Finished generating sync lists...\n";
		
		$s3 = new S3Utils($this->bucket);
		$s3->progress($this->progress);
		print_r($syncList);
		
		if (isset($syncList["upload"])) {		
			foreach ($syncList["upload"] as $file) {
				$target = $file; //Remote
				$source = $file; //Local
				
				if (!is_null($this->local)) {
					$target = str_replace($this->local, "", $file);
					$source = $this->local . "/$file";
				}
				if ($target[0] == "/") {
					$target = Utils::charStripFirst($target);
				}
				
				if (!is_null($this->remote)) {
					$target = $this->remote . "/$target";
				}
				
				echo "$source ==> $target 0%.";
				$res = $s3->objectUpload($this->domain, $source, $target, false, AmazonS3::ACL_PRIVATE, AmazonS3::STORAGE_REDUCED);
				echo ".100%\n";
			}
		}
		
		if (isset($syncList["download"])) {		
			foreach ($syncList["download"] as $file) {
				$target = $file; //Local
				$source = $file; //Remote

				if (!is_null($this->local)) {
					$target = str_replace($this->local, "", $file);
					$target = $this->local . "/$file";
				}
				
				if (!is_null($this->remote)) {
					$source = $this->remote . "/$source";
				}
				
										
				echo "$source ==> $target 0%.";
				$res = $s3->objectDownload($source, $target);
				echo ".100%\n";
			}
		}
		
		if (isset($syncList["localDelete"])) {		
			foreach ($syncList["localDelete"] as $file) {
				$target = $file;
				if (!is_null($this->local)) {
					$target = str_replace($this->local, "", $file);
					$target = $this->local . "/$file";
				}
				$res = @unlink($target);
				if ($res === false) {
					if ($this->strict === true) {
						throw new Exception (Errors::SyncProfile_MESG_LOCAL_OBJ_UNLINK_ERR . ": ($target)", Errors::SyncProfile_CODE_LOCAL_OBJ_UNLINK_ERR);
					} else {
						echo "WARNING: unable to delete local file ($file)\n";
					}
				} else {
					echo "Local file $target deleted\n";
				} 
			}
		}
		
		if (isset($syncList["remoteDelete"])) {		
			foreach ($syncList["remoteDelete"] as $file) {
				$target = $file;
				if (!is_null($this->remote)) {
					$target = $this->remote . "/$target";
				}
				$latestObj = $s3->objectGetLatest($target);
				$latestVersionID = $latestObj["versionID"];
				if (trim($latestVersionID) == "") {
					$latestVersionID = "*";
				}
				echo "Version ID: ($latestVersionID)\n";
				if ($s3->objectDelete($this->domain, $target, $latestVersionID) === true) {
					echo "Deleted $target\n";
				} else {
					echo "Failed to delete $target\n";
				}
			}
		}
			
	}
	
	private function buildLocalTree($dir) {
		if (!is_dir($dir)) {
			throw new Exception (Errors::SyncProfile_MESG_LOCAL_OBJ_NOT_DIR . ": ($dir)", Errors::SyncProfile_CODE_LOCAL_OBJ_NOT_DIR);
		}
		
		if (false === ($dh = @dir($dir))) {
			if ($this->strict === true) {
				throw new Exception (Errors::SyncProfile_MESG_LOCAL_OBJ_DIR_ERR . ": ($dir)", Errors::SyncProfile_CODE_LOCAL_OBJ_DIR_ERR);
			} else {
				echo "WARNING: can't open dir ($dir)\n";
				return;
			}
		}
		
		while (false !== ($entry = $dh->read())) {
			// skip "this" and "parent"
			if ($entry == "." || $entry == "..") { continue; }
			$fqpn = "$dir/$entry";
			
			// recurse...
			if (is_dir($fqpn)) {
				$this->buildLocalTree($fqpn);
			}
			
			if (is_file($fqpn)) {
				$this->localtree[$fqpn]["md5"] = md5_file($fqpn);
				$this->localtree[$fqpn]["mtime"] = filemtime($fqpn);
			}
		}
		$dh->close();
		if (!is_array($this->localtree)) {
			$this->localtree = array();
		}
		return true;
	}
}
?>